package util

import (
	"bytes"
	"context"
	// "crypto/sha256"
	// "encoding/hex"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	// "sort"
	"strconv"
	"strings"
	"time"

	jsoniter "github.com/json-iterator/go"
)

var json = jsoniter.ConfigCompatibleWithStandardLibrary

// Request request 请求封装
type Request interface {
	// Send 发送请求
	Send(ctx context.Context, method, url string, params map[string]interface{}, headers map[string]string, retry int) ([]byte, error)
	// ParamToQuery query 转化
	ParamToQuery(params map[string]interface{}) (string, error)
}

// GetRequest 获得 Request
func GetRequest() Request {
	return &ReqImpl{}
}

// RequestImpl request implementation
var retrySleepTime = 200 * time.Millisecond

// ReqImpl request implementation
type ReqImpl struct {
}

func fmtFieldToString(key string, val interface{}, withKey bool) (string, error) {
	var tmp string
	switch val := val.(type) {
	case string:
		tmp = val
	case int, int8, int16, int32, int64:
		tmp = strconv.FormatInt(val.(int64), 10)
	case float32, float64:
		tmp = strconv.FormatFloat(val.(float64), 'f', -1, 64)
	case bool:
		tmp = fmt.Sprintf("%v", val)
	default:
		return "", errors.New("不支持数据类型")
	}
	if withKey {
		tmp = fmt.Sprintf("%s=%s", key, tmp)
	}
	return tmp, nil
}

// ParamToQuery query
func (r *ReqImpl) ParamToQuery(params map[string]interface{}) (string, error) {
	query := url.Values{}
	for key, val := range params {
		tmp, err := fmtFieldToString(key, val, false)
		if err != nil {
			return "", err
		}
		query.Add(key, tmp)
	}
	return query.Encode(), nil
}

// func (r *ReqImpl) genSortStr(params map[string]interface{}) (string, error) {
// 	var keys []string
// 	for k := range params {
// 		if k == "sign" {
// 			return "", errors.New("sign 字段为保留字段")
// 		}
// 		keys = append(keys, k)
// 	}
// 	sort.Strings(keys)
// 	buff := make([]string, 0, len(keys))
// 	for _, key := range keys {
// 		if params[key] == "" || params[key] == nil {
// 			continue
// 		}
// 		tmp, err := fmtFieldToString(key, params[key], true)
// 		if err != nil {
// 			return "", err
// 		}
// 		buff = append(buff, tmp)
// 	}
// 	return strings.Join(buff, "&"), nil
// }

// func (r *ReqImpl) genSign(sortStr, secret string) string {
// 	// 生成 Order String
// 	signStr := sortStr + secret
// 	hash := sha256.New()
// 	_, _ = hash.Write([]byte(signStr))
// 	return hex.EncodeToString(hash.Sum(nil))
// }

func (r *ReqImpl) doRequest(ctx context.Context, method, url string, params []byte, headers map[string]string, retry int) ([]byte, error) {
	client := &http.Client{Timeout: 20 * time.Second}
	// 发送请求
	if retry == 0 {
		retry = 1
	}
	var errReq error
	var resp *http.Response
	for {
		if retry <= 0 {
			break
		}

		req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(params))
		if err != nil {
			return nil, err
		}
		for key, val := range headers {
			req.Header.Set(key, val)
		}
		resp, errReq = client.Do(req)
		if errReq != nil || resp.StatusCode == http.StatusInternalServerError {
			time.Sleep(retrySleepTime)
			retry--
		} else {
			break
		}
	}

	if errReq != nil {
		return nil, errReq
	}

	if resp == nil {
		return nil, errors.New("body is null")
	}

	defer func() {
		if resp != nil {
			_ = resp.Body.Close()
		}
	}()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return body, nil
}

// Send 发送请求
func (r *ReqImpl) Send(ctx context.Context, method, url string, params map[string]interface{}, headers map[string]string, retry int) ([]byte, error) {
	method = strings.ToUpper(method)
	var query []byte
	var err error

	if headers == nil {
		headers = make(map[string]string)
	}

	if method == "GET" || method == "DELETE" {
		queryS, err := r.ParamToQuery(params)
		if err != nil {
			return nil, err
		}
		url = fmt.Sprintf("%s?%s", url, queryS)
	} else {
		buf := &bytes.Buffer{}
		encoder := json.NewEncoder(buf)
		encoder.SetEscapeHTML(false)
		err = encoder.Encode(params)
		if err != nil {
			return nil, err
		}
		query = buf.Bytes()
		headers["Content-Type"] = "application/json"
	}

	return r.doRequest(ctx, method, url, query, headers, retry)
}
